Sines and cosines for fun and profit

Bonz

Hi everybody! This article will be a voyage through some simple effects, with a very distinctive oldskool look, all of which are obtained by combining in various clever ways the two basic trigonometric functions, sine and cosine. What's interesting is that all of these effects don't require a very deep knowledge of math, and so can be a good source of satisfaction for newbies before they attack more complex things like 3D. In the end I will present two effects that do use some math and even a little 3D projection (rotozooming and wormholes), but don't worry as I will guide you through the couple of equations you need. The only knowledge that I require is that you know how to operate in graphics mode (plot a pixel, set a palette, load a bitmap from disk and draw it).

The first effects that I present are based on the following fundamental syllogism: we want to draw nice pictures, the sine and cosine functions have nice curves, i.e. we'll use sines and cosines -- without even knowing what they are about, at least at first. Here are the curves of the sine (red) and cosine (blue):

These effects have not been used in demos for years, but they can be a useful source of inspiration in little "for fun" productions like 256 byte or 4k intros.

Plasmas

Plasmas are a very simple effect. They are nothing more than patterns that evolve over time, creating colored blobs that emerge and disappear; they are not very interesting on their own, but they can be combined in a lot of interesting ways: for example you can draw two plasmas and plot them in alternate pixels, giving a nice interference figure; or you can use them as texture or bump maps; or again, you can draw each point in the plasma as a small sprite (e.g. 8x8) instead of plotting single pixels. Anyway, I guess that every coder wrote a plasma routine once in his life, so why shouldn't you?

How do we do a plasma? A first property we must know of sines and cosines is that when you sum many of them they combine in unpredictable ways: their curves have areas in which they change very slowly and areas in which their slope is much more steep. All this, however, happens without any solution of continuity.

We will shift over time all of the curves that we sum; some of them will also depend on the x coordinate and some others on the y. The equation will be therefore something like:

        32 (cos (a x + b t + c) +
            cos (d x + e t + f) +
            cos (g y + h t + i) +
            cos (j y + k t + l) + 4)

Each cosine goes from -1 to 1, so when you sum four of them the result goes from -4 to 4. The two adjustment factors, 4 and 32, make it go from 0 to 255 instead. This is good as we will operate in a 256-color palettized mode.

I'm using only cosines because sines are simply cosines with the argument shifted by PI/2, and traditionally plasmas are coded with cosines, but sines work as well if you prefer. Of course we will not afford to compute four cosines per pixel, because another property of the sine and cosine function is that it's quite expensive to compute them (Hugi 23 had two articles by Adok and me on this topic). But sines and cosines repeat continuously as their arguments change by 2PI, so we can make a table with a few values of the cosine with the argument ranging from 0 to 2PI, and reuse the values in this table.

We will make the size of the table a nice power of two, so that the modulus can be computed with a bitwise AND, and scale the values from 0 to 63 so that we only have to sum them: the new formula is, if the cosine table has 256 entries,

          cosTable [(a x + b t + c) & 255] + 
          cosTable [(d x + e t + f) & 255] +
          cosTable [(g y + h t + i) & 255] +
          cosTable [(j y + k t + l) & 255]

Now we have to pick the values of the parameters. Well, we just try until the result satisfies us, starting from low integers and varying them until the result has a pleasing speed and look. I will give you a single hint: if two parameters have the same position in the equation (for example, both multiply a coordinate or both multiply t), don't make them equal, otherwise the curve will lose much of its unpredictability.

The pseudocode then looks like this:

     enter gfx mode and set the palette
     for i := 0 to 255 do
       cosTable[i] := 32 + 32 * cos (i * 2*PI/256)
     end;

     for each frame
       for each pixel
         plot (x,y) with a color given by the formula above

We can also economize on the multiplication by keeping track of the values of the cosines. The timeX variables track terms like b*t+c, while angleX track the arguments of the cosines (which, as you might already know, are angles):

     enter gfx mode and set the palette
     for i = 0 to 255
       cosTable[i] := 32 + 32 * cos (i * 2*PI/256)

     time1 := c
     time2 := f
     time3 := i
     time4 := l

     for each frame
       angle3 := time3
       angle4 := time4
       for y := 0 to maxy - 1 do
         angle1 := time1
         angle2 := time2
         for x := 0 to maxx - 1 do
           color := cosTable[angle1 & 255] + cosTable[angle2 & 255] + 
                    cosTable[angle3 & 255] + cosTable[angle4 & 255];
                    
           plot (x,y) with this color
           angle1 := angle1 + a
           angle2 := angle2 + d
         end;
         angle3 := angle3 + g
         angle4 := angle4 + j
       end;

       time1 := time1 + b
       time2 := time2 + e
       time3 := time3 + h
       time4 := time4 + k
     end;

That's it. You have a plasma. You can try other variations on this theme, for example:

            cos (cos (a x + b t + c) + d t + e) +
            cos (cos (f y + g t + h) + i t + j) +
                 cos (a x + b t + c) +
                 cos (f y + g t + h)

I will leave to you the task of optimizing it to remove the multiplications. Also, try to invent and code some creative combinations of plasmas, like the ones I outlined above.

Interference

Interference is another basic effect which combines sines and cosines. Instead of computing many cosines and sum them together, you use each of them as a coordinate into a texture, and sum the pixel values of the textures:

Since the textures themselves will frequently be computed using formulas based on sines and cosines, it makes sense to remove the x and y terms from the cosines, and simply turn

       256 cos (a x + b t + c)
       

into

        (x + 256 cos (b t + c)) & 255

so that what the effect does is simply to shift the textures by a different amount on each frame.

Note that with this change you can do the lookup in the cosine table once per frame, instead of once per pixel. Actually this makes it possible to remove the table altogether and use floating point arithmetic, because you only have to compute a handful of cosines per frame.

The following pseudocode assumes that textures are 256x256. To make things more interesting, I will rotate the palette on each frame, which is easily done by summing the frame number to the color of the pixel.

        x1_angle = 0; x2_angle = 0; x3_angle = 0; x4_angle = 0
        y1_angle = 0; y2_angle = 0; y3_angle = 0; y4_angle = 0
        t := 0
        
        for each frame
          increment the XX_angle by a small number (up to 0.05)
          x1 := (cos (x1_angle) + 1) * 128
          y1 := (cos (y1_angle) + 1) * 128
          x2 := (cos (x2_angle) + 1) * 128
          y2 := (cos (y2_angle) + 1) * 128
          x3 := (cos (x3_angle) + 1) * 128
          y3 := (cos (y3_angle) + 1) * 128
          x4 := (cos (x4_angle) + 1) * 128
          y4 := (cos (y4_angle) + 1) * 128
          t := t + 1
          
          for y := 0 to maxy - 1 do
            for x := 0 to maxx - 1 do
              plot (x,y) with color 
                texture1[(x1+x) & 255, (y1+y) & 255] +
                texture1[(x2+x) & 255, (y2+y) & 255] +
                texture2[(x3+x) & 255, (y3+y) & 255] +
                texture2[(x4+x) & 255, (y4+y) & 255] + t;

You can put whatever you like in this textures, for example a turbulence function (i.e. the altitude of a fractal landscape -- there are plenty of tutorials for this) or a pattern from your favorite drawing program, like 3D-Studio or the GIMP. But I will explain how to draw simple algorithmic textures with (of course) sines and cosines.

Here are the two textures that were used for the image above:

The left one is very simple. The function sqrt(x*x+y*y) will give the distance from the origin to a point (this is nothing more than Pythagoras' theorem...), and we want to give the same color to points with the same distance.

If you view the texture in a painting program and compute the distances of a few points (painting programs usually show the coordinates somewhere), you'll see that black points have a distance of 0, 64, and 128 from the origin, while white points have a distance of 63 and 127. Since the origin is at (128, 128), we must do

        for x := -128 to 127 do
          for y := -128 to 127 do
            texture[x+128, y+128] = (trunc(sqrt(x*x+y*y)) & 63) * 4
          end;
        end;
            

That's it. The other texture's formula cannot be deduced as easily, because I found it by trial and error. The formula looks like black magic, but it is simpler than it looks:

        sin (.03 (abs(x) + abs(y))) +
        sin (.03 (abs(x)^2 + abs(y)^2)) +
        sin (.03 (abs(x)^1.5 + abs(y)^1.5)) +

The result goes from -3 to 3, and must be scaled appropriately to change the range to 0..255 (you should already know how to do that from the plasma chapter).

How did I find that formula? abs(x)+abs(y) alone yields something similar to the first texture but gives a pattern of rhombs rather than circles (try to compute it for several points until you are convinced of that); putting it inside a sine avoids abrupt changes from black to white. The second term makes circles like in the first texture, but again the sine makes the shading more smooth. The last term gives rhombs with rounded edges (the exponent is half way from 1 to 2) and I added it only to make the thing more interesting.

Giving objects a trajectory

When you do effects that involve sprites, for example vectorballs or shadebobs (do you remember them?!?), you have to find a nice trajectory on which to place new objects. This is a perfect job for sines and cosines.

We'll derive the formula from the interference effect. I'll quote from that section of the article: "what the effect does is simply to shift the textures by a different amount on each frame"; in other words, what the cosines really give is the trajectory that is followed by the top-left corner of the texture -- the one at coordinates (0,0).

The formulas that we were using were

        x = cos (a t + c)
        y = cos (b t + d)
        

...and this will be perfect when a pretty simple trajectory is needed, for example for shadebobs. Here is a plot of a few functions of the form x = cos (a t), y = sin (b t), for values of a and c ranging from 3 to 6:

These curves are called the "Lissajous curves of order (a,b)". They correspond to a particular choice of the parameters c and d, that is c = d + PI/2. Also note that t can be a simple frame counter if you are using a cosine table (therefore the argument of the cosine will be from 0 to 255, or something like that), while if you use floating point math t will be incremented by a small quantity on every frame (like we did in the interference pseudocode).

Note that if a=b you get a circle... Of course it is not a coincidence, but to understand why I'll have to explain what sines and cosines actually mean.

Lissajous curves have many uses. They can be extended to three dimensions, like this:

        x = cos (a t) * cos(c t)
        y = cos (a t) * sin(d t)
        z = sin (b t)
        

If on your coding career you ever encounter "polar" and "spherical" coordinates -- you surely will -- come back here and compare the formulas for 2D and 3D Lissajous curves to those for polar and spherical coordinates, respectively. You'll find a striking similarity.

Also, you can use Lissajous curves whenever you need two values to change with some regularity, it does not matter if they are not x and y values: for example you can rotate a 3D model around two axes by PI cos(a t) and PI sin(b t) radians.

Never take this information for granted, however. Experiment a lot, try to imagine what trajectories come out when you square sines and cosines, take their absolute value, multiply a few of them, and so on; then check your guesses with a spreadsheet or a math program such as Matlab or Mathematica.

Shadebobs and wormies

Now, coding a shadebob will be a breeze. Here is an example:

This is part of a very simple intro that I wrote for the 13th birthday of my girlfriend's sister (the text means "Happy Birthday" in Italian, and the shadebob is a 13).

You have to write two bitmap drawing routines that, instead of coloring a pixel with the color indicated in the bitmap, add or subtract that color to the VRAM. Never draw the shadebob with light colors, because that would surely cause an overflow when you draw it many times with similar coordinates; in general, don't exceed a pixel value of 16.

Then, you have to ensure that only a certain number of copies of the shadebob are drawn at any time. To do so, the following pseudocode simply keeps an history of the last 64 places where the shadebob has been drawn:

    t = 0
    for every frame
        oldx = buffer[t & 63].x
        oldy = buffer[t & 63].y
        if t > 63 then
            subtract the shadebob at coordinates (oldx, oldy)
          
        newx = cos(a t)
        newy = sin(b t)
        buffer[t & 63].x = newx
        buffer[t & 63].y = newy
        add the shadebob at coordinates (newx, newy)
        t = t + 1

Here is the actual routine that I used in my intro. The change was needed to make the 13 readable, and is very simple: the latest position is overdrawn five times (to make the bob lighter) and made dimmer on the very next frame:

    t = 0
    for every frame
        if t >= 1 then
            subtract the shadebob at coordinates (newx, newy)
            subtract the shadebob at coordinates (newx, newy)
            subtract the shadebob at coordinates (newx, newy)
            subtract the shadebob at coordinates (newx, newy)
            
        oldx = buffer[t & 63].x
        oldy = buffer[t & 63].y
        if t > 63 then
            subtract the shadebob at coordinates (oldx, oldy)
          
        newx = cos(a t)
        newy = sin(b t)
        buffer[t & 63].x = newx
        buffer[t & 63].y = newy
        add the shadebob at coordinates (newx, newy)
        add the shadebob at coordinates (newx, newy)
        add the shadebob at coordinates (newx, newy)
        add the shadebob at coordinates (newx, newy)
        add the shadebob at coordinates (newx, newy)
        t = t + 1

Note that the balance between drawn and removed copies is preserved: on each frame, five copies of the shadebob are drawn and five are removed.

Wormies are very similar, but have more complex trajectories. Also, usually you redraw wormies from scratch on each frame because their components are opaque (the most recent ones overlap the older ones).

This one is taken from a 4k intro, "Never bored", by Ritz (see Hugi 21):

This one can easily become a complex effect, but there is very little math in it apart from sin/cos. As usual, you can get by with a little trial and error; here is a simple wormie:

        x = cos t + .7 * cos 3.02t
        y = sin t + .05 * sin 15.04t

My first step here was to take a circle and perturb it with a Lissajous curve of order (3,15). Then I also tweaked the order of the curve a bit to break its symmetry and make the wormie's behavior more interesting.

You can invent many variations on wormies like you can do with plasmas. For example, Ritz's wormie is three-dimensional and the balls that compose it are scaled so that the farthest are also the smallest. Also, the simplest possible wormie draws each ball for a fixed number of frames and then removes it, while Ritz makes the balls fade out slowly and also doesn't let them stay fixed at their original position, but moves them away as they get dimmer (I think).

Trigonometry? What's trigonometry?

Let's go back to Lissajous curves. I already pointed out that a curve of the form

        x = cos a t
        y = sin a t

is a circle, and that it is not a coincidence. In fact, by definition, if you have a circle of radius 1 and an angle x, (cos x, sin x) is the point on the circle whose radius makes an angle of x with the x axis:

Angles are measured in a funny unit of measure that mathematicians love, called radian, with the property that 2PI radians equal exactly 360 degrees (a full tour around the circle).

Now we can describe any point on a circle of radius R by the angle a that it forms, by writing (x, y) as (R cos a, R sin a). These are called polar coordinates and will be useful for the next two effects, rotozooming and wormholes.

Rotozooming (also known as rototiling)

This effect is quite easily done and offers an easy way to test your understanding of basic "real" applications of sines and cosines (i.e. exploiting their definition and not treating them as simply nice curves). This screenshot is another section of my birthday intro:

Let's write a point in polar coordinates and rotate it by an angle da. It should be clear that the point's cartesian coordinates can be expressed as

        x = R cos (a)
        y = R sin (a)
        
        x' = R cos (a + da)
        y' = R sin (a + da)

respectively before and after rotation.

Now take for granted two trigonometric formulas that give the sine and cosine of the sum of two angles:

        cos (a + da) = cos(a) cos(da) - sin(a) sin(da)
        sin (a + da) = cos(a) sin(da) + sin(a) cos(da)

and plug it into the formulas for x' and y':

        x' = R cos(a) cos(da) - R sin(a) sin(da)
        y' = R cos(a) sin(da) + R sin(a) cos(da)

But is it worthwhile to map from cartesian to polar coordinates and back, twice for pixel? It looks like a good deal of work, and of course it is not necessary, because we can substitute the original x and y like this:

        x' = x cos(da) - y sin(da)
        y' = x sin(da) + y cos(da)

The beauty is that cos(da) and sin(da) are constant, and hence can be computed only once per frame!

For now, let's content of rotating every pixel in the bitmap without zooming. We could apply the equations that we have just found to obtain the screen (i.e. rotated) coordinates for each point of the bitmap, but this wouldn't do the tiling effect that is seen above and would not extend easily to zooming (you would plot many points to the same destination when each the image was made smaller, and leave holes when the image is enlarged).

Instead we want to proceed through all the pixels on screen and, one by one, find where it comes from on the texture image. There are two ways to find the required equation, i.e. brute force and reasoning. Brute force means solving the equations above for x and y rather than x' and y'; not so complicated, but if you think about it, you simply have to invert the rotation direction to reverse the transformation: if texture->screen is a clockwise rotation, screen->texture must be a counterclockwise rotation. So the equations are simply:

        x = x' cos(da) - y' sin(da)
        y = x' sin(da) + y' cos(da)

And here is the pseudocode. Why I am using a table to store sines and cosines will be clear later. Also note that I am not doing the four multiplications on every pixel, and that you must compensate so that the rotation is around the center of the screen rather than around the top-left corner:

     enter gfx mode and set the palette
     for i := 0 to 255 do
       cosTable[i] := cos (i * 2*PI/256)
       sinTable[i] := sin (i * 2*PI/256)
     end;
     
     angle := 0
     for each frame
       cosine := cosTable[angle]
       sine := sinTable[angle]
       
       for sy := 0 to maxy - 1 do
         tx := (-maxx / 2) * cosine - (-maxy / 2) * sine
         ty := (-maxx / 2) * sine + (-maxy / 2) * cosine
         for sx := 0 to maxx - 1 do
           color := texture[tx][ty];
           plot (sx,sy) with this color
           tx := tx + cosine
           ty := ty + sine
         end;
       end;
     end;

Now we want to add zooming as well. The equations for zooming are simply

        x' = k x
        y' = k y

with k>1 giving enlarged images and k<1 giving smaller images. These are easily inverted to x = x'/k, y = y'/k to transform screen coordinates to texture coordinates. We can also combine them with the equations for rotations:

        x = x' cos(da) / k - y' sin(da) / k
        y = x' sin(da) / k + y' cos(da) / k

Now, what we will use to vary k over time? Of course a cosine curve! For example, if you want the curve to oscillate between 50% and 250% of its original size, just put k = 1.5+sin(a t).

You still have only two coefficients in the equations: only, instead of cos(da) and sin(da), they are cos(da)/k and sin(da)/k. Turning "rototiling" into "rotozooming" is then surprisingly simple: you just have to tweak the cosine and sine table:

     for i := 0 to 255 do
       cosTable[i] := cos (i * 2*PI/256) * (1.5 + sin (i * 2*PI/256))
       sinTable[i] := sin (i * 2*PI/256) * (1.5 + sin (i * 2*PI/256))
     end;

That's it!

Starfields, or the basics of 3D projection

This effect does not have a single sine or cosine in it :-) but it is propedeutic to another one, wormholes, which has a lot of them! Starfields are yet another of those effects that everybody codes once in his life, because they are the simplest three-dimensional effect one could imagine.

A star field should give the idea of moving at (ridiculously) high speed in space, seeing stars coming towards you as you miraculously dodge all of them.

A basic starfield is very simple: you have a set of 3D points to be plotted on the screen, and the movement effect is achieved by simply decreasing the z coordinate (this shoule make sense after the paragraph on rotozooming: the imaginary spaceship moves towards increasing z coordinates, so the stars "see" the inverse transformation which is to decrease z's). and redisplaying the results. The formula for 3D to 2D projection (read: conversion) is:

     xs = d * x / z
     ys = d * y / z

This should make perfect sense. As the object moves away from you (increasing z coordinates), the screen coordinates (xs,ys) tend to (0,0). The dparameter is simply used to scale the coordinates so that the objects occupy a meaningful fraction of the screen and can be set for example to 256 or another power of two.

First of all you create a set of random stars with a fixed z and random (x,y). On each frame you decrement the z coordinates, compute the screen coordinates and, if they got out of the screen, you replace the star with a newly created one. Since far objects are also very dim, the color is usually given based on the z coordinate, with a shade going from black to light violet, or something like that.

Starfields have many variations that can make them more interesting. You can make the rendering more realistic by drawing the stars as short lines (possibly antialiased...) which give the impression of the persistence of images on the human eye; you can tilt the viewpoint so that it looks like the spaceship is really dodging the stars; you can draw the stars in different sizes depending on how near they are. To do so, you need a very basic knowledge of 3D math: on one hand, I don't have the space and the intention to treat all this here; on the other hand, actually you might be able to deduce it from what I said on 2D rotation, and anyway there are plenty of tutorials that cover that. Creating all these variations will be a great way to practice with basic 3D math.

Wormholes

The wormhole effect is a good way to end this tutorial, because it combines many of the aspects that I have covered: trajectories, 3D, and basic trigonometry. A wormhole can be seen in 2nd reality, by the Future Crew (nothing less):

To draw a wormhole, you simply have to draw many circles at different z coordinates. The perception of depth can be improved by giving darker colors for bigger z's.

The centers of the circles move on a Lissajous curve. You need to store the center coordinates for a certain number of circles, and draw them on each frame with a different z: on each frame, the z will get smaller because the circles will move towards the viewer (whose z is 0).

Now let's go down to the details. How do we draw a 3D circle? In fact it is pretty easy, because the points on the circle will have a constant z (the circle directly faces the viewer). So if the center is (x0, y0, z0) and the radius is r, the points on the circle are (x0 + r cos a, y0 + r sin a, z0). To do perspective projection, you simply have to scale appropriately x0, y0, and the radius as well.

The pseudocode for the drawcircle function is then

    drawcircle (x0, y0, z0, r, color):
      xc := d * x0 / z0
      yc := d * y0 / z0
      rc := d * r / z0
      
      for t := 0 to 63 do
        x = xc + rc * cos(t * 2*PI/64)
        y = yc + rc * sin(t * 2*PI/64)
        if (x,y) is on screen, plot it with the given color
      end;

Of course you'd better have a sine/cosine table. Now, let's go with the whole effect, using 64 circles.

     enter gfx mode and set the palette
     t := 0
     for each frame
       x0[t & 63] := cos (3*t * 2*PI/256) + 1
       y0[t & 63] := sin (5*t * 2*PI/256) + 1

       z = 200
       for i = t - 63 to t do
         if i > 0 then
           drawcircle (x0[i & 63], y0[i & 63], z, 1, color)
           z = z + 5
         end;
       end;
       t := t + 1
     end;

To understand how 3D projection works, adjust the initial value of z (200) and the value of d (not given in the source above). You should notice that smaller z's must be compensated with smaller d's, but the compensation will not be exact, because picking small values of z will distort the image like if you were using a wide-angle or fisheye lens on a camera.

If you feel brave, you can get a tutorial on drawing filled polygons and turn this wormhole into a solid one. Just compute pairs of points on two adjacent circles and draw filled quadrilaterals.

Or, learn something on 3D rotation and try to do a real 3D wormhole. That is, the trajectory will not be given directly by a Lissajous curve, but the curve will only give the angle between the z axis and the direction that the wormhole assumes (you need spherical coordinates to do that); the circles then will be tilted so that they are always perpendicular to the direction, and as you move in the wormhole the viewer will also tilt to align with the direction.

This is quite complex, so here are a couple of hints. First, you can easily compute the coordinates a circle and the direction assumed by the wormhole: store in an array both the direction of the z axis (0,0,1) and the coordinates of the points on a circle perpendicular to the z axis (r cos t, r sin t, 0); then, rotate all these points by the same angles, and magically the points on the circle will stay perpendicular to the axis.

Second, remember that angles are always relative to the position of the viewer. For example, now I'm facing the monitor of my PC (0 degrees), but if I turn my head to the left (+90 degrees), the monitor will be on my right (-90 degrees). In other words, if an object makes an angle a with the z axis, and the viewer makes an angle b with the z axis, you must rotate the object by an angle of a-b.

The reason for the - sign is similar to what we did when rotozooming: there, if we applied a transformation on the texture, we would have applied the inverse transformation to the screen coordinates; similarly, if we apply a transformation on the viewer, we must apply the opposite transformation to the objects.

What's next?

Well, the effects in this articles should give you enough material to experiment with and to receive some satisfaction from your first productions. Remember that ultimate 3D can be a goal, but it takes time, and starting with simple things will be easier and more interesting as well. People will appreciate demos with these simple effects because demos are not judged only for their technical quality, but also for their design and for their purpose.

Take a look at productions which you know to have few or no 3D scenes (there is no preferred category -- as outstanding examples of this, consider the 4k Mesha or the Ibiza demo), and try to look at them from a mathematical point of view. Of course, do this after having enjoyed the spectacle! Try to recreate what you see or some variation on it. If it looks too complex, don't worry, try something else. If you care, try some of the "exercises" that I propose, and when you feel ready, go for some 3D as well. The really important things are that you experiment a lot, and that you enjoy yourself!

--
|_  _  _ __
|_)(_)| ) ,'
-------- '-._